package server;

import analyticsServer.UserEvent;
import java.io.IOException;
import java.rmi.RemoteException;
import java.security.PrivateKey;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import security.exception.HandShakeFailedException;
import security.MasterChannel;
import server.data.*;

/**
 * Does the communication with a sincle Auction-Client
 *
 * @author Gruppe 95
 *
 */
public class TCPServerThread implements Runnable {
    
    private static final Logger logger = Logger.getLogger("TCPServerThread");
    private final MasterChannel masterChannel;
    private final PrivateKey privateKey;
    //User-Commands
    private static final String COMM_LOGIN = "!login";
    private static final String COMM_LIST = "!list";
    private static final String COMM_LOGOUT = "!logout";
    //Auction-Commands
    private static final String COMM_CREATE = "!create";
    private static final String COMM_BID = "!bid";
    private static final String COMM_GROUP_BID = "!groupBid";
    private static final String COMM_CONFIRM = "!confirm";
    private static final String COMM_END = "!end";
    private static final String COMM_CLIENT_LIST = "!getClientList";
    private static final String COMM_CLIENT_LIST_HIDDEN = "!getClientList-hidden";
    private static final String COMM_SIGNED_BID = "!signedBid";
    private static final String OUT_SUCCESSFULLY_LOGGED_IN = "!logged-in";
    public static final String OUT_SUCCESSFULLY_LOGGED_OUT = "!logged-out";
    public static final String OUT_CLIENT_LIST = "!client-list";
    public static final String OUT_CLIENT_HIDDEN = "!hidden-client-list";
    private Server server;
    
    public TCPServerThread(final MasterChannel masterChannel,final Server server, final PrivateKey serverPrivateKey) {
        //logger.setLevel(Level.OFF);
        this.masterChannel = masterChannel;
        logger.log(Level.INFO, "create masterchannel {0} ", new Object[]{masterChannel});
        this.privateKey = serverPrivateKey;
        this.server=server;
    }

    /**
     * Run TCP ServerThread for one Client.
     *
     */
    @Override
    public void run() {
        try {
            logger.severe("Running tcpserverthread");
            String strLine;
            
            while (!server.getStopped() && !server.getInterrupted()) {
                if (masterChannel.receivable()) {
                    strLine = masterChannel.receiveMessage();
                    logger.log(Level.INFO, "master channel receive message {0} ", new Object[]{strLine});
                    final String strParts[] = strLine.split(" ");
                    
                    if (masterChannel.getUser() == null) {
                        if (strParts[0].equals(COMM_LIST)) {
                            masterChannel.sendMessage(AuctionManager.getInstance().list());
                        } else {
                            logger.info("call handshake");
                            try {
                                masterChannel.doServerHandShake(privateKey);
                                masterChannel.sendMessage(OUT_SUCCESSFULLY_LOGGED_IN + " " + masterChannel.getUser().toString());
                            } catch (HandShakeFailedException ex) {
                                logger.info("handshake failed server");
                                masterChannel.sendMessage(ex.getMessage());
                            }
                        }
                        
                        
                    } else if (masterChannel.getUser() != null) {
                        if (strParts[0].equals(COMM_LOGIN)) {
                            masterChannel.sendMessage("User " + masterChannel.getUser().getStrName() + "is already logged in.");
                        } else if (strParts[0].equals(COMM_LIST)) {
                            String output = AuctionManager.getInstance().list();
                            masterChannel.sendMessage(output + " " + masterChannel.createHMAC(output));
                            logger.info("send list command trough channel");
                        } else if (strParts[0].equals(COMM_END)) {
                            stop();
                            //                } else if (user == null && (strParts[0].equals(COMM_LOGOUT) || strParts[0].equals(COMM_CREATE)
                            //                        || strParts[0].equals(COMM_BID))) {
                            //                    masterChannel.sendMessage("You have to login first!");
                        } else if (strParts[0].equals(COMM_LOGOUT)) {
                            logger.info("logout client on server - set back channel");
                            masterChannel.logoutServer();
                        } else if (strParts[0].equals(COMM_CREATE)) {
                            createAuction(strLine);
                        } else if (strParts[0].equals(COMM_BID)) {
                            bidOnAuction(strParts);
                        } else if (strParts[0].equals(COMM_GROUP_BID)) {
                            bidOnGroupAuction(strParts);
                        } else if (strParts[0].equals(COMM_CONFIRM)) {
                            confirmGroupBid(strParts);
                        } else if (strParts[0].equals(COMM_CLIENT_LIST)) {
                            masterChannel.sendMessage(OUT_CLIENT_LIST + UserManager.getInstance().getClientList());
                        } else if (strParts[0].equals(COMM_CLIENT_LIST_HIDDEN)) {
                            masterChannel.sendMessage(OUT_CLIENT_HIDDEN + UserManager.getInstance().getClientList());
                        } else if (strParts[0].equals(COMM_SIGNED_BID)) {
                           // doSignedBid(strParts);
                        }else {
                            masterChannel.sendMessage("Unknown Command!");
                        }
                    }
                }
                synchronized (UserManager.getInstance()) {
                    List<String> list;
                    if ((list = UserManager.getInstance().getArrNotifications(this.masterChannel.getUser())) != null) {
                        for (String string : list) {
                            masterChannel.sendMessage(string);
                             
                        }
                        UserManager.getInstance().getArrNotifications(this.masterChannel.getUser()).clear();
                    }
                }
            }
        } catch (IOException e) {
            logger.log(Level.SEVERE, "IOException occured while listening for Client-Messages!", e);
        }
        stop();
        System.out.println("TCPServerThread stops...");
    }

    /**
     * Stops the current TCPServerThread, closes the Sockets and logs the User
     * out. Inform AnalyticsServer that User is disconnected
     */
    private void stop() {
        logger.info("Call TCPServerThread stop, Client Disconnected.");
        masterChannel.close();
        if (masterChannel.getUser() != null) {
            UserManager.getInstance().logout(masterChannel.getUser().getStrName(), masterChannel.getUser().getIAClientAddress(), masterChannel.getUser().getiPClientPort());
            Server.callAnalyticsServer(new UserEvent(Server.NAME, UserEvent.USER_DISCONNECTED, System.currentTimeMillis(), masterChannel.getUser().getStrName()));
        } /*else {
         logger.info("Stop TCPServerThread, user disconnected, user = null");
         Server.callAnalyticsServer(new UserEvent(Server.NAME, UserEvent.USER_DISCONNECTED, System.currentTimeMillis(), "UNKNOWN USER"));
                
         }*/
    }

    /**
     * Logs a User in, sends event to AnalyticsServer
     *
     * @param strLine contains out of command, username and udp-port
     */
//    private void login(String[] strLine) throws RemoteException, IOException {
//        if (masterChannel.getUser() != null) {
//            logger.info("login: user already logged in");
//            masterChannel.sendMessage(user.getStrName() + " is already logged in on this client!");
//
//        } else {
//            try {
//                String strUser = strLine[1];
//                int udpport = Integer.parseInt(strLine[2]);
//                if ((user = UserManager.getInstance().login(strUser, clientSocket.getInetAddress(), udpport)) != null) {
//                    masterChannel.sendMessage(OUT_SUCCESSFULLY_LOGGED_IN + strUser);
//                    Server.callAnalyticsServer(new UserEvent(Server.NAME, UserEvent.USER_LOGIN, System.currentTimeMillis(), strUser));
//                    //UserManager.getInstance().sendNotifications(user);
//                } else {
//                    masterChannel.sendMessage(strUser + " is already logged in somewhere!");
//                }
//            } catch (IndexOutOfBoundsException ex) {
//                masterChannel.sendMessage("Invalid parameters, Usage: !login <username>!");
//            } catch (NumberFormatException nex) {
//                masterChannel.sendMessage("Invalid parameters, error parsing the udp-Port!");
//            }
//        }
//    }
    /**
     * Logs the current User out, informs AnalyticsServer.
     *
     * @param strLine contains out of command, username and udp-port
     */
//    private void logout() throws RemoteException, IOException {
//        if (UserManager.getInstance().logout(user.getStrName(), user.getIAClientAddress(), user.getiPClientPort()) == null) {
//            //should never be reached
//            masterChannel.sendMessage("You have to log in first!");
//        } else {
//            masterChannel.sendMessage(OUT_SUCCESSFULLY_LOGGED_OUT + user.getStrName());
//            Server.callAnalyticsServer(new UserEvent(Server.NAME, UserEvent.USER_LOGOUT, System.currentTimeMillis(), user.getStrName()));
//            this.user = null;
//        }
//    }
    /**
     * Creates a new Auction
     *
     * @param strLine contains out of command, auction runtime in seconds and
     * auction description
     */
    private void createAuction(final String strLine) throws IOException {
        try {
            int iTimeStart = strLine.indexOf(' ') + 1;
            int iTimeEnd = strLine.indexOf(' ', iTimeStart);
            long lTime = Long.parseLong(strLine.substring(iTimeStart, iTimeEnd));
            String strDescription = strLine.substring(iTimeEnd + 1);
            
            if (strDescription.length() == 0) {
                masterChannel.sendMessage("You have to give the auction a description!");
            } else {
                Auction auction = AuctionManager.getInstance().create(lTime, strDescription, masterChannel.getUser());
                if (auction != null) {
                    StringBuilder strB = new StringBuilder(100);
                    strB.append("An auction '").append(auction.getStrDesc()).append("' with id ").append(auction.getlId()).append(" has been created and will end on ").
                            append(AuctionManager.getInstance().formatDate(auction.getdEnd())).append(".");
                    masterChannel.sendMessage(strB.toString());
                }
            }
        } catch (IndexOutOfBoundsException iox) {
            masterChannel.sendMessage("Invalid parameters, Usage: !create <duration> <description>!");
        } catch (NumberFormatException nex) {
            masterChannel.sendMessage("Invalid parameters, the first parameter (the duration in seconds) has to be a number!");
        }
    }

    /**
     * Bids on a existing Auction, informs AnalyticsServer
     *
     * @param strLine contains out of command, Auction id and Amount to bid
     */
    private void bidOnAuction(final String[] strLine) throws RemoteException, IOException {
        try {
            final Bid bid = new Bid(strLine, masterChannel.getUser());
            synchronized (AuctionManager.getInstance()) {
                final Auction auction = AuctionManager.getInstance().bid(bid);
                if (auction != null) {
                    if (bid.getState().equals(Bid.OVERBID)) {
                        masterChannel.sendMessage("You successfully bid with " + AuctionManager.getInstance().formatDouble(bid.getdPrice()) + " on '" + auction.getStrDesc() + "'.");
                    } else if (bid.getState().equals(Bid.UNDERBID)) {
                        masterChannel.sendMessage("You unsuccessfully bid with " + AuctionManager.getInstance().formatDouble(bid.getdPrice()) + " on '" + auction.getStrDesc()
                                + "'. Current highest bid is " + AuctionManager.getInstance().formatDouble(auction.getHighestBid().getdPrice()) + ".");
                    }
                } else {
                    masterChannel.sendMessage("Auction-id '" + bid.getAuctionId() + "' does not exist!");
                }
            }
        } catch (InvalidCommandException ex) {
            masterChannel.sendMessage(ex.getMessage());
        }
    }

    
    /**
     * Bids on an existing Auction with a Signed Bid
     *
     * @param strParts String [] strLine[0]="!signedBid", strLine[1]=<auction_id>, strLine[2]=<amount>, strLine[3]=<user:timestamp1:signature1>, strLine[4]=<user:timestamp2:signature2> 
     */
    private void doSignedBid(final String[] strParts) throws RemoteException, IOException {
        try {
            final SignedBid bid = new SignedBid(strParts, masterChannel.getUser());
            synchronized (AuctionManager.getInstance()) {
                if(bid.isFitting())
                {
                    final Auction auction = AuctionManager.getInstance().bid(bid);
                    if (auction != null) {
                        if (bid.getState().equals(Bid.OVERBID)) {
                            masterChannel.sendMessage("You successfully bid with " + AuctionManager.getInstance().formatDouble(bid.getdPrice()) + " on '" + auction.getStrDesc() + "'.");
                        } else if (bid.getState().equals(Bid.UNDERBID)) {
                            masterChannel.sendMessage("You unsuccessfully bid with " + AuctionManager.getInstance().formatDouble(bid.getdPrice()) + " on '" + auction.getStrDesc()
                                    + "'. Current highest bid is " + AuctionManager.getInstance().formatDouble(auction.getHighestBid().getdPrice()) + ".");
                        }
                        else if(bid.getState().equals(Bid.NO_AUCTION))
                        {
                            masterChannel.sendMessage("Auction-id '" + bid.getAuctionId() + "' was already finished!");
                        }
                    } else {
                        masterChannel.sendMessage("Auction-id '" + bid.getAuctionId() + "' does not exist!");
                    }
                }
            }
        } catch (InvalidCommandException ex) {
            masterChannel.sendMessage(ex.getMessage());
        }
    }    
    /**
     * Handles the !bidGroup <auction_id> <amount> Message. Creates a new
     * GroupBid if <amount> is higher than the current highest bid and the count
     * of Auctions with group-bids is not higher than the user count
     *
     * @param strLine String [] strLine[0]="!groupBid", strLine[1]=<auction_id>,
     * strLine[2]=<amount>
     * @throws RemoteException
     * @throws IOException
     */
    private void bidOnGroupAuction(final String[] strLine) throws RemoteException, IOException {
        synchronized (AuctionManager.getInstance()) {
            int userCount=UserManager.getInstance().getLoggedOnUserCount();
            if (userCount >= 3 && userCount>AuctionManager.getInstance().getAuctionWGroupBidCount()) {
                try {
                    final GroupBid bid = new GroupBid(strLine, masterChannel.getUser());

                    //@todo look for confirmation

                    final Auction auction = AuctionManager.getInstance().groupBid(bid);
                    if (auction != null) {
                        if (bid.getState().equals(Bid.UNDERBID)) {
                            masterChannel.sendMessage("!rejected Please use a higher bid price.");
                        }
                    } else {
                        masterChannel.sendMessage("!rejected Auction-id '" + bid.getAuctionId() + "' does not exist!");
                    }
                } catch (InvalidCommandException ex) {
                    masterChannel.sendMessage(ex.getMessage());
                }
            } else {
                masterChannel.sendMessage("!rejected Not enough users online.");
            }
        }
    }

    /**
     * Handles the !confirm <auction_id> <amount> <username> Message. If it is
     * the first confirm message for a GroupBid a Timer of 20 seconds is
     * started, if there is no second confirm message until this Timer runs out,
     * a !rejected message is send to the bidder of the GroupBid and to the
     * first confirmer. If it is the second confirm message !confirm is sent to
     * the bidder and the 2 confirmers. If there already have been 2 confirm
     * messages or the GroupBid doesn't exist !reject is sent to the confirmer.
     *
     * @param strLine String [] strLine[0]="!groupBid", strLine[1]=<auction_id>,
     * strLine[2]=<amount>, strLine[3]=<username>
     * @throws RemoteException
     * @throws IOException
     */
    private void confirmGroupBid(final String[] strLine) throws RemoteException, IOException {
        try {
            final Confirmation confirmation = new Confirmation(strLine, masterChannel.getUser());
            synchronized (AuctionManager.getInstance()) {
                AuctionManager.getInstance().confirm(confirmation);
            }
        } catch (InvalidCommandException ex) {
            masterChannel.sendMessage("!reject "+ex.getMessage());
        }
    }    
}
